package club.jdiy.core.storage;

import club.jdiy.utils.FileUtils;
import club.jdiy.utils.IdWorker;
import club.jdiy.utils.Md5Utils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;

import static java.io.File.separator;

/**
 * JDiy 服务器本机文件存储实现类.
 *
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20200725
 */
@SuppressWarnings("ALL")
public class FileStore extends StoreSaver implements Store {

    public FileStore(String contextPathUrl, File uploadDir, String storeBucket, String id) {
        this.contextPathURL = contextPathUrl;
        this.uploadDir = uploadDir;
        this.storeBucket = storeBucket;
        this.dataId = id;
        idPath = uploadDir.getPath() + separator + storeBucket + separator + id.substring(id.length() - 1) + separator + id;
    }

    @Override
    String getDataId() {
        return dataId;
    }

    @Override
    public StoreMetaData getStoreData() {
        StoreMetaData storeMetaData = new StoreMetaData(StoreType.file, null, new HashMap<>());
        FnFilter ff = new FnFilter();
        File[] idDir = new File(idPath).listFiles(ff);
        String ext;
        if (idDir != null) {
            for (File dir : idDir) {
                if (dir.isDirectory()) {
                    File[] files = dir.listFiles(ff);
                    if (files != null && files.length > 0) {
                        List<StoreMetaData.StoreItem> ls = new ArrayList<>();
                        for (File f : files) {
                            ext = f.getName().substring(f.getName().lastIndexOf(".") + 1).toLowerCase();
                            ls.add(new StoreMetaData.StoreItem(f.getName(), f.length(), ext));
                        }
                        storeMetaData.getItems().put(dir.getName(), ls);
                    }
                }
            }
        }
        return storeMetaData;
    }

    /**
     * view层调用核心方法．使用方式：
     * 1. DBEntity继承 {@link AbstractStorble} 或实现 {@link Storable}接口.
     * 2. 然后在Controller中调用 JpaEntity对象的.init方法
     * 3. view中取得单一附件（例如获取listPic附件的下载地址）:   ${vo.store.listPic}  或者：  ${vo.store['listPic']}
     * view中取得多个附件（例如获取listPic附件的下载地址）:  ${vo.store.listPic.items[0]}
     *
     * @param field 附件字段名．对应于 ${vo.store.xxx}中的xxx
     * @return Entry
     */
    //不要更改和移除(view层调用核心方法),
    // 可以直接使用 store[field]来获取对应的Attch
    //获取单个附件字段的store
    public Store.Entry get(String field) {
        File[] fs = getFiles(field);
        Store.EntryItem[] entries = new Store.EntryItem[fs.length];
        if (fs != null && fs.length > 0) {
            int i = 0;
            for (File f : fs) {
                String p = f.getPath().replaceAll("\\\\", "/");
                entries[i++] = new Store.EntryItem(
                        new StoreMetaData.StoreItem(f.getName(), f.length(), f.getName().substring(f.getName().lastIndexOf(".") + 1).toLowerCase()),
                        contextPathURL + "/uploads/" + p.substring(uploadDir.getPath().length() + 1)
                );
            }
        }
        return new Store.Entry(entries);
    }

    public String getUrl(String field) {
        Store.Entry e = get(field);
        return e == null ? null : e.toString();
    }

    public String[] getUrls(String field) {
        Store.Entry e = get(field);
        if (e == null) return null;
        int i = 0;
        String[] urls = new String[e.getItems().length];
        for (Store.EntryItem ei : e.getItems()) urls[i++] = ei.getUrl();
        return urls;
    }

    private File[] getFiles(String field) {
        File fieldDir = getFieldDir(field);
        if (!fieldDir.exists() && !fieldDir.isDirectory()) return new File[0];
        File[] fs = fieldDir.listFiles(new FnFilter());
        return fs == null ? new File[0] : fs;
    }

    public void set(String field, File file) throws IOException {
        this.set(field, file, null);
    }

    public void set(String field, File file, String fileName) throws IOException {
        if (file == null || field == null || "".equals(field) || !file.exists() || file.isDirectory()) return;
        if (fileName == null || "".equals(fileName)) fileName = file.getName();
        del(field);
        File fieldDir = getFieldDir(field);
        //noinspection ResultOfMethodCallIgnored
        fieldDir.mkdirs();
        FileUtils.copy(file, new File(fieldDir.getPath() + separator + fileName), true);
        return;
    }

    public void set(String field, MultipartFile mFile) throws IOException {
        this.set(field, mFile, null);
    }

    public void set(String field, MultipartFile mFile, String fileName) throws IOException {
        this.set(field, mFile, fileName, null);
    }

    public void set(String field, MultipartFile mFile, String fileName, Consumer<File> c) throws IOException {
        __saveFile(field, mFile, fileName, false, c);
    }

    public void add(String field, MultipartFile mFile) throws IOException {
        this.add(field, mFile, null);
    }

    public void add(String field, MultipartFile mFile, String fileName) throws IOException {
        this.add(field, mFile, fileName, null);
    }

    public void add(String field, MultipartFile mFile, String fileName, Consumer<File> c) throws IOException {
        __saveFile(field, mFile, fileName, true, c);
    }

    public void del(String field) {
        if (field == null || "".equals(field)) return;
        File fieldDir = getFieldDir(field);
        if (fieldDir.exists()) FileUtils.delAll(fieldDir);
    }

    public void del(String field, String fn) {
        File fieldDir = getFieldDir(field);
        File file = new File(fieldDir.getPath() + separator + fn);
        if (file.exists()) {
            //noinspection ResultOfMethodCallIgnored
            file.delete();
            FileUtils.delEmpty(fieldDir);
            FileUtils.delEmpty(fieldDir.getParentFile());
            FileUtils.delEmpty(fieldDir.getParentFile().getParentFile());
            FileUtils.delEmpty(fieldDir.getParentFile().getParentFile().getParentFile());
        }
    }

    public void add(String field, File file) throws IOException {
        this.add(field, file, null);
    }

    public void add(String field, File file, String fileName) throws IOException {
        if (file == null || field == null || "".equals(field) || !file.exists() || !file.isFile()) return;
        File fieldDir = new File(idPath + separator + field);
        boolean autoRename = fileName == null || "".equals(fileName);
        if (autoRename) fileName = file.getName();
        String ext = file.getName().substring(file.getName().lastIndexOf(".") + 1).toLowerCase();
        if (!fileName.toLowerCase().endsWith("." + ext)) fileName = fileName + "." + ext;

        if (autoRename && !Pattern.matches("^[a-zA-Z0-9_\\-.~@()]+$", fileName)) {
            if ("flv".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext)) {
                fileName = Md5Utils.md5(fileName).substring(8, 24) + "." + ext;//这些扩展名需要强制更名.注意要幂等，同一文件再次上传直接覆盖已上传的同名文件
            }
        }
        //noinspection ResultOfMethodCallIgnored
        fieldDir.mkdirs();
        File saveTo = new File(fieldDir.getPath() + separator + fileName);
        FileUtils.copy(file, saveTo, true);
    }

    public void delAll() {
        File f = new File(idPath);
        if (f.exists() && f.isDirectory()) {
            File p = f.getParentFile();
            File pp = p.getParentFile();
            FileUtils.delAll(f);
            FileUtils.delEmpty(p);
            FileUtils.delEmpty(pp);
        }
    }

    @Override
    public String put(MultipartFile file, String fileName) throws IOException {
        String field = IdWorker.getId() + "";
        __saveFile(field, file, fileName, true, null);
        return getUrl(field);
    }

    @Override
    public String put(MultipartFile file, String fileName, Consumer<File> c) throws IOException {
        String field = IdWorker.getId() + "";
        __saveFile(field, file, fileName, true, c);
        return getUrl(field);
    }

    @Override
    public String put(File file, String fileName) throws IOException {
        String field = IdWorker.getId() + "";
        set(field, file, fileName);
        return getUrl(field);
    }

    @Override
    public String put(byte[] bytes, String fileName) throws IOException {
        String field = IdWorker.getId() + "";
        File fieldDir = getFieldDir(field);
        fieldDir.mkdirs();
        File saveTo = new File(fieldDir.getPath() + separator + fileName);

        BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(saveTo));
        bos.write(bytes);
        bos.flush();
        bos.close();

        return getUrl(field);
    }

    private File __saveFile(String field, MultipartFile mFile, String fileName, boolean isAdd, Consumer<File> c) throws IOException {
        if (mFile == null || mFile.isEmpty()) return null;

        if (fileName == null || "".equals(fileName)) fileName = mFile.getOriginalFilename();
        String ext = mFile.getOriginalFilename().substring(mFile.getOriginalFilename().lastIndexOf(".") + 1).toLowerCase();
        if (!fileName.toLowerCase().endsWith("." + ext)) fileName = fileName + "." + ext;

        if (!Pattern.matches("^[a-zA-Z0-9_\\-.~@()]+$", fileName)) {
            if ("flv".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext)) {
                fileName = Md5Utils.md5(fileName).substring(8, 24) + "." + ext;//这些扩展名需要强制更名.注意要幂等，同一文件再次上传直接覆盖已上传的同名文件
            }
        }

        if (!isAdd) del(field);
        File fieldDir = getFieldDir(field);
        if (!fieldDir.exists()) //noinspection ResultOfMethodCallIgnored
            fieldDir.mkdirs();
        File saveTo = new File(fieldDir.getPath() + separator + fileName);
        if (saveTo.exists()) //noinspection ResultOfMethodCallIgnored
            saveTo.delete();
        try (InputStream inputStream = mFile.getInputStream(); OutputStream outputStream = new FileOutputStream(saveTo)) {
            byte[] bs = new byte[1024];
            int res;
            while ((res = inputStream.read(bs)) != -1) {
                outputStream.write(bs, 0, res);
            }
            outputStream.flush();
        }
        if (c != null) c.accept(saveTo);
        return saveTo;
    }


    private File getFieldDir(String field) {
        return new File(idPath + separator + field);
    }

    private static class FnFilter implements FilenameFilter {
        @Override
        public boolean accept(File dir, String name) {
            String nm = name.toLowerCase();
            return !nm.equals("thumbs.db") && !nm.endsWith(".jsp");
        }
    }

    private final File uploadDir;
    private final String storeBucket;
    private final String dataId;
    private final String idPath;
    private final String contextPathURL;
}

